Projet final du Kit Big Data 2022¶

In [1]:
import numpy as np
import pandas as pd
import matplotlib
import matplotlib.pyplot as plt
import seaborn as sns
import requests
import multiprocessing
from bs4 import BeautifulSoup
from shutil import copyfileobj
from urllib import request
import time
import pylightxl as xl
import glob
import datetime
import re
import warnings
import plotly.express as px
import plotly.graph_objects as go
from scipy import stats
from sklearn.model_selection import train_test_split
from statsmodels.tsa.stattools import adfuller
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf, plot_predict
from statsmodels.tsa.arima.model import ARIMA
from tqdm.notebook import tqdm
warnings.filterwarnings('ignore')

I - Acquisition et chargement des données¶

1.1. Acquisition des fichiers de classement¶

In [2]:
def download_excel(url, filename):
    ''' Télécharge le fichier Excel à l`URL donnée, et le stocke dans le fichier filename '''
    with request.urlopen(url) as response, open(filename, 'wb') as out_file:
        copyfileobj(response, out_file)

On remarque que les fichiers Excel contenant la liste des dates de classement sont disponibles sur la page principale du site https://www.vendeeglobe.org/fr/classement, et que le champ value des balises option contient le nom du fichier Excel de chaque classement.
Pour récupérer l'ensemble des fichiers Excel, il suffit donc de récupérer les noms de fichier dans les tags option, puis de lancer une requête spécifique pour les télécharger.

In [3]:
html = requests.get("https://www.vendeeglobe.org/fr/classement").content.decode('utf-8')
soup = BeautifulSoup(html)
options = soup.findAll("option")
ranking_dates = []
for opt in options:
    if opt["value"]:
        ranking_dates.append(opt["value"])
print(f"{len(ranking_dates)} fichiers de classement trouvés.")

params = [(f"https://www.vendeeglobe.org/download-race-data/vendeeglobe_{date}.xlsx", f"vendeeglobe/{date}.xlsx") for date in ranking_dates]
with multiprocessing.Pool() as p:
    p.starmap(download_excel, params)
706 fichiers de classement trouvés.

Les fichiers ont été téléchargés en local, on cherche à présent à les importer dans deux Dataframe pandas : un pour les skippers encore en course, l'autre pour les skippers ayant déjà franchis la ligne d'arrivée.
Xlwings ne fonctionnant pas sur mon ordinateur, j'ai utilisé la library pylightxl pour pouvoir ouvrir les fichiers Excel et importer les données sous pandas.

In [4]:
def extract_running_data(datetime, db, row_start, row_stop):
    running = []
    headers = db.ws(ws="fr").row(row=row_start)[:-1]
    headers = [h.split("\n")[0] for h in headers]
    headers[0:4] = ["datetime", "rank", "Nat/Voile", "Skipper/Bateau"]
    headers[-1:] = ["DTF", "DTL"]
    headers[7:11] = [f"last30m_{h}" for h in headers[7:11]]
    headers[11:15] = [f"lastranking_{h}" for h in headers[11:15]]
    headers[15:19] = [f"last24h_{h}" for h in headers[15:19]]
    running.append(headers)
    for i in range(row_start+1, row_stop):
        skip = db.ws(ws="fr").row(row=i)[1:]
        skip.insert(0, datetime)
        running.append(skip)
    return running
In [5]:
def extract_finished_data(datetime, db, row_stop):
    finished = []
    headers = db.ws(ws="fr").row(row=5)
    headers = [h.split("\n")[0] for h in headers]
    headers[0:6] = ["datetime", "rank", "Nat/Voile", "Skipper/Bateau", "Date d'arrivée", "Temps de course"]
    headers[12:15:2] = [f"gap_{h}" for h in headers[12:15:2]]
    headers[16:18] = [f"ortho_{h}" for h in headers[16:18]]
    headers[18] = "percent"
    headers[19:21] = [f"ground_{h}" for h in headers[19:21]]
    headers = [h for h in headers if h]
    finished.append(headers)
    for i in range(6, row_stop):
        skip = db.ws(ws="fr").row(row=i)[1:]
        skip.insert(0, datetime)
        skip = [skip[idx] for idx in [0, 1, 2, 3, 7, 8, 13, 15, 16, 17, 18, 19, 20]]
        finished.append(skip)
    return finished
In [6]:
def extract_data(file):
    ''' Extracts data from Excel files and stores them into 2 lists of list'''
    with open(file, 'rb') as f:
        datetime = f.name.split(".")[0].split("/")[1]
        db = xl.readxl(f)
    # On compte le nombre de fois où "Rang" apparaît dans la colonne B
    B = db.ws(ws="fr").col(col=2)
    indices = [idx for idx, value in enumerate(B) if value == "Rang\nRank"]
    
    if datetime == "20201108_120200":
        #La course n'a pas encore commencé, aucune information utile dans ce fichier
        return [], []
    
    if len(indices) > 1 :
    # Cas où des skippers ont passé la ligne, mais il y a encore des skippers en course
        # Extract les données sur les skippers encore en course
        row_start = indices[1] + 2
        row_stop = B.index("Traitements et calculs : Géovoile, un service Hauwell Studios", row_start) + 1
        running = extract_running_data(datetime, db, row_start, row_stop)
        # Extract les données sur les skippers déjà arrivés
        finished = extract_finished_data(datetime, db, row_start-1)
    elif datetime == "20210305_080000":
    # Cas où tous les skippers sont arrivés (ou ont abandonné)
        running = []
        finished = extract_finished_data(datetime, db, 39)
    else:
    # Cas où aucun skipper n'a encore franchi la ligne d'arrivée
        finished = []
        running = extract_running_data(datetime, db, 5, 39)

    return finished, running
In [7]:
runnings, arrived = [], []
files = glob.glob("vendeeglobe/*")
for file in files:
    finished, running = extract_data(file)
    if finished:
        dff = pd.DataFrame(finished[1:], columns=finished[0])
        arrived.append(dff)
    if running:
        dfr = pd.DataFrame(running[1:], columns=running[0])
        runnings.append(dfr)

df_running = pd.concat(runnings)
df_finished = pd.concat(arrived)
print(f"Classement pour les skippers encore dans la course")
display(df_running.head())
print(f"Classement pour les skippers ayant franchis la ligne d'arrivée")
display(df_finished.head())
Classement pour les skippers encore dans la course
datetime rank Nat/Voile Skipper/Bateau Heure FR Latitude Longitude last30m_Cap last30m_Vitesse last30m_VMG ... lastranking_Cap lastranking_Vitesse lastranking_VMG lastranking_Distance last24h_Cap last24h_Vitesse last24h_VMG last24h_Distance DTF DTL
0 20201230_110000 1 \nFRA 17 Yannick Bestaven\nMaître Coq IV 11:30 FR\n 55°47.17'S 110°11.00'W 78° 16.2 kts 14.5 kts ... 75° 17.1 kts 15.1 kts 51.4 nm 114° 12.1 kts 12.1 kts 290.0 nm 8435.6 nm 0.0 nm
1 20201230_110000 2 \nFRA 79 Charlie Dalin\nAPIVIA 11:30 FR\n 53°47.14'S 112°32.49'W 85° 19.6 kts 17.2 kts ... 82° 21.3 kts 18.0 kts 63.8 nm 85° 15.0 kts 12.8 kts 360.9 nm 8553.0 nm 117.4 nm
2 20201230_110000 3 \nFRA 59 Thomas Ruyant\nLinkedOut 11:30 FR\n 54°12.00'S 114°10.44'W 89° 19.9 kts 18.3 kts ... 85° 19.4 kts 17.4 kts 58.3 nm 92° 15.6 kts 14.5 kts 374.4 nm 8596.2 nm 160.6 nm
3 20201230_110000 4 \nFRA 1000 Damien Seguin\nGroupe APICIL 11:30 FR\n 54°58.86'S 114°41.63'W 90° 17.7 kts 16.8 kts ... 92° 17.3 kts 16.6 kts 51.8 nm 93° 15.4 kts 14.6 kts 369.6 nm 8596.8 nm 161.2 nm
4 20201230_110000 5 \nFRA 01 Jean Le Cam\nYes we Cam ! 11:30 FR\n 54°58.66'S 116°27.85'W 95° 9.0 kts 8.8 kts ... 96° 10.1 kts 9.9 kts 30.4 nm 92° 14.0 kts 13.3 kts 336.5 nm 8654.7 nm 219.1 nm

5 rows × 21 columns

Classement pour les skippers ayant franchis la ligne d'arrivée
datetime rank Nat/Voile Skipper/Bateau Date d'arrivée Temps de course gap_Au premier gap_Au précédent ortho_Vitesse ortho_Distance percent ground_Vitesse ground_Distance
0 20210226_170000 1\nARV \nFRA 17 Yannick Bestaven\nMaître Coq IV 28/01/2021 04:19:46 FR 80j 03h 44min 46s\n-10h 15min 00s 12.6 kts 24365.7 nm 117.3 % 14.8 kts 28583.8 nm
1 20210226_170000 2\nARV \nFRA 79 Charlie Dalin\nAPIVIA 27/01/2021 20:35:47 FR 80j 06h 15min 47s\n 02h 31min 01s 02h 31min 01s 12.6 kts 24365.7 nm 119.6 % 15.1 kts 29135.0 nm
2 20210226_170000 3\nARV \nFRA 18 Louis Burton\nBureau Vallée 2 28/01/2021 00:45:12 FR 80j 10h 25min 12s\n 06h 40min 26s 04h 09min 25s 12.6 kts 24365.7 nm 117.6 % 14.8 kts 28650.0 nm
3 20210226_170000 4\nARV \nFRA 01 Jean Le Cam\nYes we Cam ! 28/01/2021 20:19:55 FR 80j 13h 44min 55s\n-16h 15min 00s 10h 00min 09s 03h 19min 43s 12.5 kts 24365.7 nm 112.9 % 14.1 kts 27501.5 nm
4 20210226_170000 5\nARV \nMON 10 Boris Herrmann\nSeaexplorer - Yacht Club De Mo... 28/01/2021 11:19:45 FR 80j 14h 59min 45s\n-06h 00min 00s 11h 14min 59s 01h 14min 50s 12.6 kts 24365.7 nm 116.8 % 14.7 kts 28448.5 nm

1.2. Acquisition des caractéristiques des bateaux¶

La page listant les caractéristiques des bateaux est https://www.vendeeglobe.org/fr/glossaire

In [8]:
html = requests.get("https://www.vendeeglobe.org/fr/glossaire").content.decode('utf-8')
soup = BeautifulSoup(html)
boats = []
popups = soup.findAll("div", attrs={"class": "sv-u-1 sv-u-s-1-2 sv-u-m-1-3 sv-u-l-1-6"})
for elem in popups:
    boat = {}
    boat["skipper_name"] = elem.find("span", attrs={"class": "boats-list__skipper-name"}).text
    skipper_id = elem.find_next("div", attrs={"class": "boats-list__popup mfp-hide"})["id"]
    boat["skipper_id"] = skipper_id.split("-")[-1]
    boat["Nom du bateau"] = elem.find_next('h3').text
    spec_list = elem.find_next('ul').find_all('li')
    for spec in spec_list:
        key, value = spec.text.split(":")[:2]
        if " / Assemblage" in value:
            value = value.split("/")[0]
        boat[key.strip()] = value.strip()
    boats.append(boat)
       
df_boats = pd.DataFrame(boats)
df_boats
Out[8]:
skipper_name skipper_id Nom du bateau Numéro de voile Anciens noms du bateau Architecte Chantier Date de lancement Longueur Largeur Tirant d'eau Déplacement (poids) Nombre de dérives Hauteur mât Voile quille Surface de voiles au près Surface de voiles au portant
0 Fabrice AMEDEO 93 NEWREST - ART & FENÊTRES FRA 56 No Way Back, Vento di Sardegna VPLP/Verdier Persico Marine 01 Août 2015 18,28 m 5,85 m 4,50 m 7 t foils 29 m monotype 320 m2 570 m2
1 Romain ATTANASIO 110 PURE - Best Western® FRA 49 Gitana Eighty, Synerciel, Newrest-Matmut Bruce Farr Design Southern Ocean Marine (Nouvelle Zélande) 08 Mars 2007 18,28m 5,80m 4,50m 9t 2 28m acier forgé 280 m2 560 m2
2 Alexia BARRIER 100 TSE - 4MYPLANET FRA72 Famille Mary-Etamine du Lys, Initiatives Coeur... Marc Lombard MAG France 01 Mars 1998 18,28m 5,54m 4,50m 9t 2 29 m acier 260 m2 580 m2
3 Yannick BESTAVEN 91 Maître CoQ IV 17 Safran 2 - Des Voiles et Vous Verdier - VPLP CDK Technologies 12 Mars 2015 18,28 m 5,80 m 4,50 m 8 t foils 29 m acier mécano soudé 310 m2 550 m2
4 Jérémie BEYOU 103 CHARAL 08 NaN VPLP CDK Technologies 18 Août 2018 18,28 m 5,85 m 4,50 m 8t foils 29 m acier 320 m2 600 m2
5 Arnaud BOISSIÈRES 88 LA MIE CÂLINE - ARTISANS ARTIPÔLE FRA 14 Ecover3, Président, Gamesa, Kilcullen Voyager-... Owen Clarke Design LLP - Clay Oliver Hakes Marine - Mer Agitée 03 Août 2007 18,28 m 5,65 m 4,50 m 7,9 tonnes foils 29 m basculante avec vérin 300 m² 610 m²
6 Louis BURTON 106 BUREAU VALLEE 2 18 Banque Populaire VIII Verdier - VPLP CDK Technologies 09 Juin 2015 18,28 m 5,80 m 4,50 m 7,6 t foils 28 m acier 300 m2 600 m2
7 Didac COSTA 123 ONE PLANET ONE OCEAN ESP 33 Kingfisher - Educacion sin Fronteras - Forum M... Owen Clarke Design Martens Yachts 02 Février 2000 18,28 m 5,30 m 4,50 m 8,9 t 2 26 m acier 240 m2 470 m2
8 Manuel COUSIN 92 GROUPE SÉTIN FRA 71 Paprec-Virbac2, Estrella Damm, We are Water, L... Bruce Farr Yacht Design Southern Ocean Marine (Nouvelle-Zélande) 02 Février 2007 18,28 m 5,80 m 4,50 m 9 t 2 asymétriques 28,50 basculante sur vérin hydraulique 270 m2 560 m2
9 Clarisse CREMER 95 BANQUE POPULAIRE X FRA30 Macif - SMA Verdier - VPLP CDK - Mer Agitée 01 Mars 2011 18,28 m 5,70 m 4,5 m 7,7 t 2 29 m acier forgé 340 m2 570 m2
10 Charlie DALIN 105 APIVIA FRA 79 NaN Verdier CDK technologies - MerConcept 05 Août 2019 18,28 m 5,85 m 4,50 m 8 t foils 29 m acier 350 m2 560 m2
11 Sam DAVIES 97 INITIATIVES-COEUR FRA109 Maître CoQ - Banque Populaire VII- Foncia II VPLP - Verdier CDK Technologies 20 Septembre 2010 18,28 m 5,70 m 4,50 m 7,8 t foils 27 m acier forgé 300 m2 600 m2
12 Sébastien DESTREMAU 117 MERCI 69 Foresight Natural Energy, Maisonneuve Lavanos Artech do Brasil 15 Janvier 2005 18,28 m 5,60 m 4,50 m 8,5 t 2 29 m acier 250 m2 650 m2
13 Benjamin DUTREUX 112 OMIA - WATER FAMILY FRA09 Spirit of Yukoh, Neutrogena, Hugo Boss, Estrel... Bruce Farr Design Offshore Challenge - Cowes 03 Juillet 2007 18,28 m 5,85 m 4,50 m 8 t 2 29 m acier forgé 300 m2 700 m2
14 Kevin ESCOFFIER 89 PRB FRA 85 NaN Verdier - VPLP CDK Technologies 08 Mars 2010 18,28 m 5,50 m 4,50 m NC foils 27,40 m Acier mécano soudé 300 m2 600 m2
15 Clément GIRAUD 114 Compagnie du Lit / Jiliti FRA83 Delta Dore, Bureau Vallée, Vers un Monde sans ... Bruce Farr design JMV Cherbourg 26 Juillet 2006 18,28 m 5,75 m 4,50 m 8,5 t 2 29 m acier forgé 300 m2 620 m2
16 François Guiffant 129 NaN NaN NaN NaN 01 Janvier 1970 NaN NaN NaN NaN NaN NaN NaN NaN NaN
17 Pip HARE 120 MEDALLIA GBR77 Armor Lux, We Are Water, La Fabrique Pierre Rolland Bernard Stamm 03 Juillet 1999 18,28 m 5,70 m 4,50 m 9 t 2 29 m carbone 300 m2 580 m2
18 Boris HERRMANN 109 SEAEXPLORER - YACHT CLUB DE MONACO 16 Edmond de Rothschild, Malizia - Yacht Club de ... Verdier - VPLP Multiplast 07 Août 2015 18,28 m 5,70 m 4,50 m 7,6 t foils 29 m acier 290 m2 490 m2
19 Ari HUUSELA 124 STARK FIN222 Aviva, GAES Centros Auditivos Owen Clarke Design Hakes Marine - Wellington (Nouvelle-Zélande) 06 Août 2007 18,28 m 5,80 m 4,50 m 8,5 t 2 28 m acier 270 m2 580 m2
20 Isabelle JOSCHKE 102 MACSF FRA 27 Quéguiner, Safran Verdier - VPLP Chantier Naval de Larros 04 Juillet 2007 18,28 m 5,60 m 4,50 m 7,7 t foils 27 m acier 300 m2 650 m2
21 Jean LE CAM 116 Yes We Cam! 001 Cheminées Poujoulat, Mare, Maître CoQ, Mapfre,... Bruce Farr design CDK Technologies 03 Janvier 2007 18,28 m 5,90 m 4,50 m 8 t 2 28 m acier forgé 300 m2 620 m2
22 Stéphane LE DIRAISON 94 TIME FOR OCEANS FRA 92 Hugo Boss, Energa, Compagnie du Lit-Boulogne B... Finot-Conq Design Neville Hutton 01 Juin 2007 18,28 m 5,84 m 4,50 m 8,5 t foils 28 m monotype 300 m2 550 m2
23 Miranda MERRON 104 CAMPAGNE DE FRANCE FRA50 Great America IV, Mirabaud, Temenos Owen Clarke Southern Ocean Marine, Tauranga 04 Mai 2006 18,28 m 5,50 m 4,50 m 8,5 t 2 28 m carbone 330 m2 600 m2
24 Giancarlo PEDOTE 96 PRYSMIAN GROUP ITA 34 Saint-Michel - Virbac VPLP - Verdier Multiplast 02 Avril 2015 18,28 m 5,80 m 4,5 m 8 t foils 29 m acier forgé 300 m2 600 m2
25 Alan ROURA 99 LA FABRIQUE SUI07 Brit Air, Votre Nom autour du Monde, MACSF Groupe Finot-Conq Multiplast 01 Août 2007 18,28 m 5,90 m 4,50 m nc foils 27 m carbone 290 m2 580 m2
26 Thomas RUYANT 113 LinkedOut NaN NaN Verdier Persico 03 Septembre 2019 18,28 m 5,85 m 4,50 m 8 t foils 29 m acier forgé 350 m2 560 m2
27 Damien SEGUIN 111 GROUPE APICIL FRA1000 Comme Un Seul Homme, DCNS Groupe Finot-Conq Multiplast 10 Août 2008 18,28 m 5,85 m 4,50 m 8,5 t 2 29 m acier forgé 350 m2 610 m2
28 Kojiro SHIRAISHI 119 DMG MORI Global One JPN 11 NaN VPLP Multiplast 05 Septembre 2019 18,28 m 5,85 m 4,50 m 8 t foils 29 m acier forgé 320 m2 580 m2
29 Sébastien SIMON 90 ARKEA PAPREC 4 NaN Juan Kouyoumdjian CDK Technologies 19 Juillet 2019 18,28 m 5,70 m 4,50 m 8 t foiler 29 m Inox usiné 260 m2 600 m2
30 Maxime SOREL 107 V and B-MAYENNE FRA53 Le Souffle du Nord, Groupe Bel Verdier - VPLP Indiana Yachting (Scarlino, Italie) 07 Septembre 2007 18,28 m 5,50 m 4,50 m 7,7 t 2 29 m acier 365 m2 700 m2
31 Alex THOMSON 122 HUGO BOSS GBR 99 NaN VPLP - Alex Thomson Racing (led by Pete Hobson) Carrington Boats 15 Août 2019 18,28 m 5,40 m 4,50 m 7,7 t foils 29 m acier forgé 330 m2 630 m2
32 Armel TRIPON 101 L'OCCITANE EN PROVENCE 2 NaN Samuel Manuard Black Pepper© 31 Janvier 2020 18,28 m 5,50 m 4,50 m 7,8 t foils 28 m acier forgé 270 m2 535 m2
33 Nicolas TROUSSEL 108 CORUM L'EPARGNE FRA 6 NaN Juan Kouyoumdjian CDK technologies - Mer Agitée 15 Mai 2020 18,28 m 5,70 m 4,50 m 7,9 t foils 27,30 m NaN 270 m2 535 m2

II - Préparation des données¶

2.1. Nettoyage des données de classement¶

In [9]:
def clean_ranking_running(df):
    # Formattage des dates
    df["datetime"] =  pd.to_datetime(df["datetime"], format="%Y%m%d_%H%M00")

    # Convertit le rang en chiffre pour pouvoir faire des moyennes dessus
    # Les valeurs non numériques pour les abandons / retards au départ sont converties en Nan
    df["rank"] = pd.to_numeric(df['rank'], errors="coerce").astype(float)
    
    # Séparation des nationalités et voiles
    # Le champ Nationalité est presque toujours vide et non utile, il n'est pas gardé
    df.insert(3, "Voile", df['Nat/Voile'].apply(lambda x: x.split("\n")[1]))

    # Séparation des skippers et bateaux
    df.insert(4, "Skipper", df['Skipper/Bateau'].apply(lambda x: x.split("\n")[0]))
    df.insert(5, "Bateau", df['Skipper/Bateau'].apply(lambda x: x.split("\n")[1]))
    df.drop(columns=['Nat/Voile', 'Skipper/Bateau', 'Heure FR'], inplace=True)

    # Conversion des coordonnées en valeurs décimales
    def convert_degrees(s):
        if s != "":
            degrees = re.split('[°\']', s)
            return (float(degrees[0]) + (float(degrees[1])/60)) * (-1 if degrees[2] == 'S' or degrees[2] == 'W' else 1)   
    df['Latitude'] = df['Latitude'].apply(convert_degrees)
    df['Longitude'] = df['Longitude'].apply(convert_degrees)

    # Conversion des grandeurs physiques en nombres
    for col in ['last30m_Cap', 'lastranking_Cap', 'last24h_Cap']:
        df[col] = df[col].astype(str).str[:-1].astype(float, errors="ignore")
    for col in ['last30m_Vitesse', 'lastranking_Vitesse', 'last24h_Vitesse',
                'last30m_VMG', 'lastranking_VMG', 'last24h_VMG',
                'last30m_Distance', 'lastranking_Distance', 'last24h_Distance',
                'DTF', 'DTL']:
        #df[col] = df[col].apply(lambda x: x.split(' ')[0]).astype(float, errors="ignore")
        df[col] = pd.to_numeric(df[col].apply(lambda x: x.split(' ')[0]), errors="coerce").astype(float)
    
    # Tri par ordre de dates et rang
    df = df.sort_values(by=["datetime", "rank"], ascending=[True,True], axis=0, ignore_index=True)
    
    return df    
In [10]:
df_running = clean_ranking_running(df_running)
df_running
Out[10]:
datetime rank Voile Skipper Bateau Latitude Longitude last30m_Cap last30m_Vitesse last30m_VMG ... lastranking_Cap lastranking_Vitesse lastranking_VMG lastranking_Distance last24h_Cap last24h_Vitesse last24h_VMG last24h_Distance DTF DTL
0 2020-11-08 14:00:00 1.0 FRA 18 Louis Burton Bureau Vallée 2 46.407667 -1.841333 241 17.7 17.5 ... 357 0.0 0.0 2788.0 201 0.3 0.3 6.1 24293.9 0.0
1 2020-11-08 14:00:00 2.0 MON 10 Boris Herrmann Seaexplorer - Yacht Club De Monaco 46.405667 -1.830333 241 11.1 10.9 ... 357 0.0 0.0 2787.9 196 0.3 0.2 6.0 24294.2 0.4
2 2020-11-08 14:00:00 3.0 FRA 8 Jérémie Beyou Charal 46.415167 -1.833167 244 15.5 15.5 ... 357 0.0 0.0 2788.5 199 0.2 0.2 5.5 24294.3 0.5
3 2020-11-08 14:00:00 4.0 FRA 59 Thomas Ruyant LinkedOut 46.411833 -1.828000 244 13.2 13.1 ... 357 0.0 0.0 2788.3 196 0.2 0.2 5.6 24294.5 0.6
4 2020-11-08 14:00:00 5.0 FRA 53 Maxime Sorel V And B Mayenne 46.409833 -1.826000 246 10.9 10.9 ... 357 0.0 0.0 2788.1 195 0.8 0.7 5.8 24294.5 0.6
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
19222 2021-03-05 04:00:00 NaN FRA 109 Samantha Davies Initiatives - Coeur NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN
19223 2021-03-05 04:00:00 NaN FRA 4 Sébastien Simon ARKEA PAPREC NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN
19224 2021-03-05 04:00:00 NaN GBR 99 Alex Thomson HUGO BOSS NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN
19225 2021-03-05 04:00:00 NaN FRA 85 Kevin Escoffier PRB NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN
19226 2021-03-05 04:00:00 NaN FRA 6 Nicolas Troussel CORUM L'Épargne NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN

19227 rows × 21 columns

In [11]:
def clean_ranking_finished(df):
    # Formattage des dates
    df["datetime"] =  pd.to_datetime(df["datetime"], format="%Y%m%d_%H%M00")
    df["Date d'arrivée"] = df["Date d'arrivée"].apply(lambda x: pd.to_datetime(x.split(" FR")[0], format="%d/%m/%Y %H:%M:%S") if x!= "" else x)
    df["Temps de course"] = df["Temps de course"].apply(lambda x: x.split("\n")[0] if "\n" in x else x)

    # Séparation des rangs et statuts
    df.insert(1, "Rang", pd.to_numeric(df['rank'].apply(lambda x: x.split("\n")[0]), errors="coerce").astype(float))
    df.insert(2, "Statut", df['rank'].apply(lambda x: x.split("\n")[1] if "\n" in x else x))
    
    # Séparation des nationalités et voiles
    # Le champ Nationalité est presque toujours vide et non utile, il n'est pas gardé
    df.insert(3, "Voile", df['Nat/Voile'].apply(lambda x: x.split("\n")[1]))

    # Séparation des skippers et bateaux
    df.insert(4, "Skipper", df['Skipper/Bateau'].apply(lambda x: x.split("\n")[0]))
    df.insert(5, "Bateau", df['Skipper/Bateau'].apply(lambda x: x.split("\n")[1]))
    df.drop(columns=['rank', 'Nat/Voile', 'Skipper/Bateau'], inplace=True)

    # Conversion des grandeurs physiques en nombres
    for col in ['ortho_Vitesse', 'ortho_Distance', 'percent', 'ground_Vitesse', 'ground_Distance']:
        #df[col] = df[col].apply(lambda x: x.split(' ')[0]).astype(float, errors='ignore')
        df[col] = pd.to_numeric(df[col].apply(lambda x: x.split(' ')[0]), errors="coerce").astype(float)
    
    # Tri par ordre de dates et rang
    df = df.sort_values(by=["datetime", "Rang"], ascending=[True,True], axis=0, ignore_index=True)
    
    return df   
In [12]:
df_finished = clean_ranking_finished(df_finished)
df_finished
Out[12]:
datetime Rang Statut Voile Skipper Bateau Date d'arrivée Temps de course gap_Au premier gap_Au précédent ortho_Vitesse ortho_Distance percent ground_Vitesse ground_Distance
0 2021-01-27 17:00:00 1.0 ARV FRA 79 Charlie Dalin APIVIA 2021-01-27 21:35:47 80j 06h 15min 47s 12.6 24355.0 119.6 15.1 29135.0
1 2021-01-27 21:00:00 1.0 ARV FRA 17 Yannick Bestaven Maître Coq IV 2021-01-28 05:19:46 80j 03h 44min 46s 12.6 24365.7 117.3 14.8 28583.8
2 2021-01-27 21:00:00 2.0 ARV FRA 79 Charlie Dalin APIVIA 2021-01-27 21:35:47 80j 06h 15min 47s 02h 31min 01s 02h 31min 01s 12.6 24365.7 119.6 15.1 29135.0
3 2021-01-27 21:00:00 3.0 ARV FRA 18 Louis Burton Bureau Vallée 2 2021-01-28 01:45:12 80j 10h 25min 12s 06h 40min 26s 04h 09min 25s 12.6 24365.7 117.6 14.8 28650.0
4 2021-01-28 04:00:00 1.0 ARV FRA 17 Yannick Bestaven Maître Coq IV 2021-01-28 05:19:46 80j 03h 44min 46s 12.6 24365.7 117.3 14.8 28583.8
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
3934 2021-03-05 08:00:00 NaN RET FRA 109 Samantha Davies Initiatives - Coeur NaT NaN NaN NaN NaN NaN
3935 2021-03-05 08:00:00 NaN RET FRA 4 Sébastien Simon ARKEA PAPREC NaT NaN NaN NaN NaN NaN
3936 2021-03-05 08:00:00 NaN RET GBR 99 Alex Thomson HUGO BOSS NaT NaN NaN NaN NaN NaN
3937 2021-03-05 08:00:00 NaN RET FRA 85 Kevin Escoffier PRB NaT NaN NaN NaN NaN NaN
3938 2021-03-05 08:00:00 NaN RET FRA 6 Nicolas Troussel CORUM L'Épargne NaT NaN NaN NaN NaN NaN

3939 rows × 15 columns

2.2. Nettoyage des caractéristiques des bateaux¶

Les noms des skippers ne sont pas orthographiés de la même manière entre les données de classement et de bateau. On les extrait des données de classement pour faire un mapping dessus dans la table des bateaux

In [13]:
skippers = df_finished["Skipper"].unique().tolist()
first_names = [name.split(" ")[0] for name in skippers]
names, counts = np.unique(first_names, return_counts=True)
print(f"Prénoms en double : {names[np.where(counts > 1)][0]}")
Prénoms en double : Sébastien

Seul le prénom Sébastien est en double, se baser sur les prénoms pour faire le mapping devrait donc se faire sans trop de problèmes.

Dans le dataframe des bateaux, un des bateaux n'a pas de numéro de voile. Peut-être peut-on le récupérer depuis le dataframe des classements

In [14]:
voiles = df_finished[["Skipper", "Voile"]]
voiles[voiles["Skipper"]=="Thomas Ruyant"].iloc[0]
Out[14]:
Skipper    Thomas Ruyant
Voile             FRA 59
Name: 7, dtype: object

Le numéro de voile correspondant est donc FRA 59

Certains Nan sont présents dans des champs non utiles (ex: Anciens noms du bateau), ils ne sont donc pas nettoyés explicitement.

In [15]:
def clean_boats_features(df_boats, skipper_mapping):
    # Drop François Guiffant (seulement un nom, aucune caractéristique de bateau, n'intervient pas dans la course)
    df_boats = df_boats.drop(16).reset_index(drop=True)
    
    # Conversion des grandeurs physiques en nombres
    for col in ['Longueur', 'Largeur', 'Tirant d\'eau', 'Déplacement (poids)', 'Hauteur mât', 'Surface de voiles au près', 'Surface de voiles au portant']:
        df_boats[col] = df_boats[col].apply(lambda x: re.sub(' ?[A-Za-z]+[2²]?', '', x).replace(',', '.')).astype(float, errors='ignore')
    
    # Mapping des noms avec ceux des données de classement pour merge
    mapping = {}
    skippers = df_boats["skipper_name"].unique().tolist()
    for skip in skippers:
        if skip == "Sébastien DESTREMAU":
            mapping[skip] = "Sébastien Destremau"
        elif skip == "Sébastien SIMON":
            mapping[skip] = "Sébastien Simon"
        elif skip == "Sam DAVIES":
            mapping[skip] = "Samantha Davies"
        else:
            mapping[skip] = [name for name in skipper_mapping if name.split(" ")[0] == skip.split(" ")[0]][0]
    df_boats["skipper_name"] = df_boats["skipper_name"].map(mapping)
    df_boats[df_boats["skipper_name"] == "Thomas Ruyant"]["Numéro de voile"] = "FRA 59"
    
    # Fix foils bateau SIMON
    df_boats = df_boats.replace("foiler", "foils")
    
    return df_boats
In [16]:
df_boats = clean_boats_features(df_boats, skippers)
df_boats
Out[16]:
skipper_name skipper_id Nom du bateau Numéro de voile Anciens noms du bateau Architecte Chantier Date de lancement Longueur Largeur Tirant d'eau Déplacement (poids) Nombre de dérives Hauteur mât Voile quille Surface de voiles au près Surface de voiles au portant
0 Fabrice Amedeo 93 NEWREST - ART & FENÊTRES FRA 56 No Way Back, Vento di Sardegna VPLP/Verdier Persico Marine 01 Août 2015 18.28 5.85 4.5 7 foils 29.0 monotype 320.0 570.0
1 Romain Attanasio 110 PURE - Best Western® FRA 49 Gitana Eighty, Synerciel, Newrest-Matmut Bruce Farr Design Southern Ocean Marine (Nouvelle Zélande) 08 Mars 2007 18.28 5.80 4.5 9 2 28.0 acier forgé 280.0 560.0
2 Alexia Barrier 100 TSE - 4MYPLANET FRA72 Famille Mary-Etamine du Lys, Initiatives Coeur... Marc Lombard MAG France 01 Mars 1998 18.28 5.54 4.5 9 2 29.0 acier 260.0 580.0
3 Yannick Bestaven 91 Maître CoQ IV 17 Safran 2 - Des Voiles et Vous Verdier - VPLP CDK Technologies 12 Mars 2015 18.28 5.80 4.5 8 foils 29.0 acier mécano soudé 310.0 550.0
4 Jérémie Beyou 103 CHARAL 08 NaN VPLP CDK Technologies 18 Août 2018 18.28 5.85 4.5 8 foils 29.0 acier 320.0 600.0
5 Arnaud Boissieres 88 LA MIE CÂLINE - ARTISANS ARTIPÔLE FRA 14 Ecover3, Président, Gamesa, Kilcullen Voyager-... Owen Clarke Design LLP - Clay Oliver Hakes Marine - Mer Agitée 03 Août 2007 18.28 5.65 4.5 7.9 foils 29.0 basculante avec vérin 300.0 610.0
6 Louis Burton 106 BUREAU VALLEE 2 18 Banque Populaire VIII Verdier - VPLP CDK Technologies 09 Juin 2015 18.28 5.80 4.5 7.6 foils 28.0 acier 300.0 600.0
7 Didac Costa 123 ONE PLANET ONE OCEAN ESP 33 Kingfisher - Educacion sin Fronteras - Forum M... Owen Clarke Design Martens Yachts 02 Février 2000 18.28 5.30 4.5 8.9 2 26.0 acier 240.0 470.0
8 Manuel Cousin 92 GROUPE SÉTIN FRA 71 Paprec-Virbac2, Estrella Damm, We are Water, L... Bruce Farr Yacht Design Southern Ocean Marine (Nouvelle-Zélande) 02 Février 2007 18.28 5.80 4.5 9 2 asymétriques 28.5 basculante sur vérin hydraulique 270.0 560.0
9 Clarisse Cremer 95 BANQUE POPULAIRE X FRA30 Macif - SMA Verdier - VPLP CDK - Mer Agitée 01 Mars 2011 18.28 5.70 4.5 7.7 2 29.0 acier forgé 340.0 570.0
10 Charlie Dalin 105 APIVIA FRA 79 NaN Verdier CDK technologies - MerConcept 05 Août 2019 18.28 5.85 4.5 8 foils 29.0 acier 350.0 560.0
11 Samantha Davies 97 INITIATIVES-COEUR FRA109 Maître CoQ - Banque Populaire VII- Foncia II VPLP - Verdier CDK Technologies 20 Septembre 2010 18.28 5.70 4.5 7.8 foils 27.0 acier forgé 300.0 600.0
12 Sébastien Destremau 117 MERCI 69 Foresight Natural Energy, Maisonneuve Lavanos Artech do Brasil 15 Janvier 2005 18.28 5.60 4.5 8.5 2 29.0 acier 250.0 650.0
13 Benjamin Dutreux 112 OMIA - WATER FAMILY FRA09 Spirit of Yukoh, Neutrogena, Hugo Boss, Estrel... Bruce Farr Design Offshore Challenge - Cowes 03 Juillet 2007 18.28 5.85 4.5 8 2 29.0 acier forgé 300.0 700.0
14 Kevin Escoffier 89 PRB FRA 85 NaN Verdier - VPLP CDK Technologies 08 Mars 2010 18.28 5.50 4.5 foils 27.4 Acier mécano soudé 300.0 600.0
15 Clément Giraud 114 Compagnie du Lit / Jiliti FRA83 Delta Dore, Bureau Vallée, Vers un Monde sans ... Bruce Farr design JMV Cherbourg 26 Juillet 2006 18.28 5.75 4.5 8.5 2 29.0 acier forgé 300.0 620.0
16 Pip Hare 120 MEDALLIA GBR77 Armor Lux, We Are Water, La Fabrique Pierre Rolland Bernard Stamm 03 Juillet 1999 18.28 5.70 4.5 9 2 29.0 carbone 300.0 580.0
17 Boris Herrmann 109 SEAEXPLORER - YACHT CLUB DE MONACO 16 Edmond de Rothschild, Malizia - Yacht Club de ... Verdier - VPLP Multiplast 07 Août 2015 18.28 5.70 4.5 7.6 foils 29.0 acier 290.0 490.0
18 Ari Huusela 124 STARK FIN222 Aviva, GAES Centros Auditivos Owen Clarke Design Hakes Marine - Wellington (Nouvelle-Zélande) 06 Août 2007 18.28 5.80 4.5 8.5 2 28.0 acier 270.0 580.0
19 Isabelle Joschke 102 MACSF FRA 27 Quéguiner, Safran Verdier - VPLP Chantier Naval de Larros 04 Juillet 2007 18.28 5.60 4.5 7.7 foils 27.0 acier 300.0 650.0
20 Jean Le Cam 116 Yes We Cam! 001 Cheminées Poujoulat, Mare, Maître CoQ, Mapfre,... Bruce Farr design CDK Technologies 03 Janvier 2007 18.28 5.90 4.5 8 2 28.0 acier forgé 300.0 620.0
21 Stéphane Le Diraison 94 TIME FOR OCEANS FRA 92 Hugo Boss, Energa, Compagnie du Lit-Boulogne B... Finot-Conq Design Neville Hutton 01 Juin 2007 18.28 5.84 4.5 8.5 foils 28.0 monotype 300.0 550.0
22 Miranda Merron 104 CAMPAGNE DE FRANCE FRA50 Great America IV, Mirabaud, Temenos Owen Clarke Southern Ocean Marine, Tauranga 04 Mai 2006 18.28 5.50 4.5 8.5 2 28.0 carbone 330.0 600.0
23 Giancarlo Pedote 96 PRYSMIAN GROUP ITA 34 Saint-Michel - Virbac VPLP - Verdier Multiplast 02 Avril 2015 18.28 5.80 4.5 8 foils 29.0 acier forgé 300.0 600.0
24 Alan Roura 99 LA FABRIQUE SUI07 Brit Air, Votre Nom autour du Monde, MACSF Groupe Finot-Conq Multiplast 01 Août 2007 18.28 5.90 4.5 foils 27.0 carbone 290.0 580.0
25 Thomas Ruyant 113 LinkedOut NaN NaN Verdier Persico 03 Septembre 2019 18.28 5.85 4.5 8 foils 29.0 acier forgé 350.0 560.0
26 Damien Seguin 111 GROUPE APICIL FRA1000 Comme Un Seul Homme, DCNS Groupe Finot-Conq Multiplast 10 Août 2008 18.28 5.85 4.5 8.5 2 29.0 acier forgé 350.0 610.0
27 Kojiro Shiraishi 119 DMG MORI Global One JPN 11 NaN VPLP Multiplast 05 Septembre 2019 18.28 5.85 4.5 8 foils 29.0 acier forgé 320.0 580.0
28 Sébastien Simon 90 ARKEA PAPREC 4 NaN Juan Kouyoumdjian CDK Technologies 19 Juillet 2019 18.28 5.70 4.5 8 foils 29.0 Inox usiné 260.0 600.0
29 Maxime Sorel 107 V and B-MAYENNE FRA53 Le Souffle du Nord, Groupe Bel Verdier - VPLP Indiana Yachting (Scarlino, Italie) 07 Septembre 2007 18.28 5.50 4.5 7.7 2 29.0 acier 365.0 700.0
30 Alex Thomson 122 HUGO BOSS GBR 99 NaN VPLP - Alex Thomson Racing (led by Pete Hobson) Carrington Boats 15 Août 2019 18.28 5.40 4.5 7.7 foils 29.0 acier forgé 330.0 630.0
31 Armel Tripon 101 L'OCCITANE EN PROVENCE 2 NaN Samuel Manuard Black Pepper© 31 Janvier 2020 18.28 5.50 4.5 7.8 foils 28.0 acier forgé 270.0 535.0
32 Nicolas Troussel 108 CORUM L'EPARGNE FRA 6 NaN Juan Kouyoumdjian CDK technologies - Mer Agitée 15 Mai 2020 18.28 5.70 4.5 7.9 foils 27.3 NaN 270.0 535.0

2.3. Rapprochement des données des voiliers avec celle des classements¶

Nous pouvons à présent combiner les dataframes des caractéristiques de bateau et de classement grâce au nom de skipper.

In [17]:
merge_df = pd.merge(left=df_running, right=df_boats, left_on="Skipper", right_on="skipper_name", how="left")
merge_df.drop(columns=['skipper_name', "Bateau"], inplace=True)
merge_df
Out[17]:
datetime rank Voile Skipper Latitude Longitude last30m_Cap last30m_Vitesse last30m_VMG last30m_Distance ... Date de lancement Longueur Largeur Tirant d'eau Déplacement (poids) Nombre de dérives Hauteur mât Voile quille Surface de voiles au près Surface de voiles au portant
0 2020-11-08 14:00:00 1.0 FRA 18 Louis Burton 46.407667 -1.841333 241 17.7 17.5 0.3 ... 09 Juin 2015 18.28 5.80 4.5 7.6 foils 28.0 acier 300.0 600.0
1 2020-11-08 14:00:00 2.0 MON 10 Boris Herrmann 46.405667 -1.830333 241 11.1 10.9 0.4 ... 07 Août 2015 18.28 5.70 4.5 7.6 foils 29.0 acier 290.0 490.0
2 2020-11-08 14:00:00 3.0 FRA 8 Jérémie Beyou 46.415167 -1.833167 244 15.5 15.5 0.5 ... 18 Août 2018 18.28 5.85 4.5 8 foils 29.0 acier 320.0 600.0
3 2020-11-08 14:00:00 4.0 FRA 59 Thomas Ruyant 46.411833 -1.828000 244 13.2 13.1 0.7 ... 03 Septembre 2019 18.28 5.85 4.5 8 foils 29.0 acier forgé 350.0 560.0
4 2020-11-08 14:00:00 5.0 FRA 53 Maxime Sorel 46.409833 -1.826000 246 10.9 10.9 0.2 ... 07 Septembre 2007 18.28 5.50 4.5 7.7 2 29.0 acier 365.0 700.0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
19222 2021-03-05 04:00:00 NaN FRA 109 Samantha Davies NaN NaN NaN NaN NaN ... 20 Septembre 2010 18.28 5.70 4.5 7.8 foils 27.0 acier forgé 300.0 600.0
19223 2021-03-05 04:00:00 NaN FRA 4 Sébastien Simon NaN NaN NaN NaN NaN ... 19 Juillet 2019 18.28 5.70 4.5 8 foils 29.0 Inox usiné 260.0 600.0
19224 2021-03-05 04:00:00 NaN GBR 99 Alex Thomson NaN NaN NaN NaN NaN ... 15 Août 2019 18.28 5.40 4.5 7.7 foils 29.0 acier forgé 330.0 630.0
19225 2021-03-05 04:00:00 NaN FRA 85 Kevin Escoffier NaN NaN NaN NaN NaN ... 08 Mars 2010 18.28 5.50 4.5 foils 27.4 Acier mécano soudé 300.0 600.0
19226 2021-03-05 04:00:00 NaN FRA 6 Nicolas Troussel NaN NaN NaN NaN NaN ... 15 Mai 2020 18.28 5.70 4.5 7.9 foils 27.3 NaN 270.0 535.0

19227 rows × 36 columns

III - Analyses et story telling¶

3.1. Visualisation du classement final¶

Il est possible d'afficher le classement final en récupérant les 33 dernières lignes du dataframe contenant les skipper ayant franchi la ligne d'arrivée. Les rang affichés en Nan correspondent aux abandons.
N.B. : Le classement final ne correspond pas exactement à l'ordre d'arrivée des skippers, certains temps de compensation ayant été accordés.

In [18]:
final_rank = df_finished[-33:][["Date d'arrivée", "Rang", "Skipper"]].reset_index(drop=True)
final_rank
Out[18]:
Date d'arrivée Rang Skipper
0 2021-01-28 04:19:46 1.0 Yannick Bestaven
1 2021-01-27 20:35:47 2.0 Charlie Dalin
2 2021-01-28 00:45:12 3.0 Louis Burton
3 2021-01-28 20:19:55 4.0 Jean Le Cam
4 2021-01-28 11:19:45 5.0 Boris Herrmann
5 2021-01-28 05:42:01 6.0 Thomas Ruyant
6 2021-01-28 12:18:20 7.0 Damien Seguin
7 2021-01-28 13:02:20 8.0 Giancarlo Pedote
8 2021-01-29 10:05:20 9.0 Benjamin Dutreux
9 2021-01-30 04:50:15 10.0 Maxime Sorel
10 2021-02-01 07:27:50 11.0 Armel Tripon
11 2021-02-03 16:44:25 12.0 Clarisse Cremer
12 2021-02-06 09:15:58 13.0 Jérémie Beyou
13 2021-02-06 17:06:02 14.0 Romain Attanasio
14 2021-02-11 08:56:06 15.0 Arnaud Boissieres
15 2021-02-11 11:52:56 16.0 Kojiro Shiraishi
16 2021-02-11 20:29:56 17.0 Alan Roura
17 2021-02-11 22:36:00 18.0 Stéphane Le Diraison
18 2021-02-12 01:57:30 19.0 Pip Hare
19 2021-02-13 20:47:03 20.0 Didac Costa
20 2021-02-16 10:28:31 21.0 Clément Giraud
21 2021-02-17 23:16:51 22.0 Miranda Merron
22 2021-02-20 08:35:40 23.0 Manuel Cousin
23 2021-02-28 07:23:44 24.0 Alexia Barrier
24 2021-03-05 08:35:46 25.0 Ari Huusela
25 NaT NaN Sébastien Destremau
26 NaT NaN Isabelle Joschke
27 NaT NaN Fabrice Amedeo
28 NaT NaN Samantha Davies
29 NaT NaN Sébastien Simon
30 NaT NaN Alex Thomson
31 NaT NaN Kevin Escoffier
32 NaT NaN Nicolas Troussel

3.2. Route des voiliers¶

A partir des données récupérées, il est à présent possible d'afficher la route de chacun des voiliers pendant la course.

In [19]:
fig = go.Figure()

df_skippers = []
for skipper in df_running["Skipper"].unique():
    df_skippers.append(df_running[df_running["Skipper"] == skipper])
for df in df_skippers:
    fig = fig.add_trace(go.Scattergeo(        
        lon = df['Longitude'],
        lat = df['Latitude'],
        text = df['datetime'].astype('str') + " : Classement " + df['rank'].astype('str'),
        mode="lines",
        name=df["Skipper"].unique()[0]))
fig.update_geos(projection_type="azimuthal equal area", fitbounds="locations")
fig.update_layout(height=600, margin={"r":0,"t":0,"l":0,"b":0})
# Option "svg" permet l'affichage de la carte sur GitHub ... mais enlève l'interactivité de la carte
# A remplacer par fig.show() pour rendre la carte interactive
fig.show("svg")
Louis BurtonBoris HerrmannJérémie BeyouThomas RuyantMaxime SorelFabrice AmedeoJean Le CamAlex ThomsonSébastien DestremauPip HareKevin EscoffierStéphane Le DiraisonRomain AttanasioDamien SeguinClément GiraudSamantha DaviesYannick BestavenGiancarlo PedoteKojiro ShiraishiNicolas TrousselManuel CousinClarisse CremerAlan RouraDidac CostaBenjamin DutreuxArmel TriponAlexia BarrierIsabelle JoschkeSébastien SimonMiranda MerronAri HuuselaCharlie DalinArnaud Boissieres

Cet affichage permet notamment de repérer l'abandon de certains skippers (Sébastien Destremeau qui s'arrête en Nouvelle-Zélande, Alex Thomson et Samantha Davies qui s'arrêtent au Cap de Bonne-Espérance, etc.)

En zoomant au sud du Cap de Bonne-Espérance, il est également possible d'observer le changement de cap de Yannick Bestaven (en bleu ci-dessou), qui a modifié sa trajectoire pour porter assistance à Kevin Escoffier (en rouge ci-dessous).
Par comparaison, Charlie Davin (en cyan) a maintenu son cap et terminé la course avant Yannick Bestaven. Un temps de compensation a par la suite été accordé à Yannick Bestaven pour le sauvetage, ce qui lui a permis de terminer 1er au classement.

In [20]:
fig = go.Figure()
df_skippers_shortlist = []
for skipper in ["Yannick Bestaven", "Kevin Escoffier", "Charlie Dalin"]:
    df_skippers_shortlist.append(df_running[df_running["Skipper"] == skipper])
for df in df_skippers_shortlist:
    fig = fig.add_trace(go.Scattergeo(        
        lon = df['Longitude'],
        lat = df['Latitude'],
        text = df['datetime'].astype('str') + " : Classement " + df['rank'].astype('str'),
        mode="lines",
        name=df["Skipper"].unique()[0]))
fig.update_geos(projection_type="natural earth", center=dict(lat=-41, lon=9), projection_scale=2.5)
fig.update_layout(height=300, margin={"r":0,"t":0,"l":0,"b":0})
fig.show("svg")
Yannick BestavenKevin EscoffierCharlie Dalin

Il est également possible de visualiser les différences d'optimisation du trajet de chaque skipper. Sur la carte ci-dessous, on peut observer qu'Alan Roura (en cyan ci-dessou) a énormément dévié vers le continent américain sur le retour, et a donc parcouru beaucoup une distance total beaucoup plus importante que Maxime Sorel (en rouge ci-dessou) par exemple.

In [21]:
fig = go.Figure()
df_skippers_shortlist = []
for skipper in ["Charlie Dalin", "Maxime Sorel", "Alan Roura"]:
    df_skippers_shortlist.append(df_running[df_running["Skipper"] == skipper])
for df in df_skippers_shortlist:
    fig = fig.add_trace(go.Scattergeo(        
        lon = df['Longitude'],
        lat = df['Latitude'],
        text = df['datetime'].astype('str') + " : Classement " + df['rank'].astype('str'),
        mode="lines",
        name=df["Skipper"].unique()[0]))
fig.update_geos(projection_type="natural earth", fitbounds="locations")
fig.update_layout(height=300, margin={"r":0,"t":0,"l":0,"b":0})
fig.show("svg")
Charlie DalinMaxime SorelAlan Roura

3.3. Evolution du classement¶

A partir des données des skippers encore en course, il est également possible de visualiser l'évolution du classement des skippers, ainsi que les dates des abandons. On remarque une grande variabilité des classements en début de course, mais la liste des skippers faisant partie du top 10 semble déjà être déterminée à partir du premier mois de course.

In [22]:
plt.figure(figsize=(20, 15))
for df in df_skippers:
    df = df.dropna()
    plt.plot(df["datetime"], df["rank"])
    plt.text(df.iloc[-1]["datetime"] + pd.Timedelta(days=1), df.iloc[-1]["rank"]-0.2,
             df["Skipper"].tolist()[0], fontsize=14)

plt.title('Evolution du classement au cours de la course', fontsize=16)
plt.xlabel('Date', fontsize=16)
plt.ylabel('Classement (avant correction avec temps compensatoire)', fontsize=16)
plt.yticks(range(34))
plt.show()

3.4. Distance parcourue par voilier¶

Il est également intéressant de regarder la distance totale parcourue par chaque skipper au long de la course.

In [23]:
plt.figure(figsize=(15, 10))
for df in df_skippers:
    plt.plot(df["datetime"], df["last24h_Distance"].cumsum(), label=df["Skipper"].tolist()[0])

plt.legend(bbox_to_anchor=(1.18,0.98))
plt.grid(True, 'major', 'both')
plt.title('Distance cumulative parcourue par skipper')
plt.xlabel('Date')
plt.ylabel('Distance en miles nautiques')
plt.show()

On retrouve bien les observations faites sur la dernière carte présentée plus haut, à savoir que certains skippers comme Alan Roura ont parcouru des distances beaucoup plus importantes pour arriver à la ligne d'arrivée (ce qui impacte logiquement leur classement).

In [24]:
plt.figure(figsize=(10, 5))
for df in df_skippers:
    if df["Skipper"].tolist()[0] in ["Charlie Dalin", "Maxime Sorel", "Alan Roura"]:
        plt.plot(df["datetime"], df["last24h_Distance"].cumsum(), label=df["Skipper"].tolist()[0])

plt.legend()
plt.grid(True, 'major', 'both')
plt.title('Distance cumulative parcourue par skipper')
plt.xlabel('Date')
plt.ylabel('Distance en miles nautiques')
plt.show()

3.5. Analyse des performances¶

Après ces premières observations, il est à présent intéressant de se pencher sur les facteurs impactant le plus le classement final. On étudiera l'impact de la vitesse utile (VMG) des voiliers, et l'impact de la présence de foils sur le classement.

3.5.1. Corrélation et régression linéaire entre le classement (rang) et la vitesse utile (VMG) des voiliers.¶

On calcule la VMG moyenne pour chaque skipper à partir des données relevées toutes les 24h pour effectuer une régression linéaire entre le classement et la VMG. Les données de VMG fournies par classement ne sont pas utilisées, car elles ne sont pas obtenues à des intervalles de temps constants.

In [25]:
df_moyennes = df_running.groupby("Skipper").mean()
df_moyennes.drop(columns=["Latitude", "Longitude", "last30m_Vitesse", "last30m_VMG", "last30m_Distance",
                          "last30m_Vitesse", "lastranking_Vitesse", "lastranking_VMG", "lastranking_Distance",
                          "lastranking_Vitesse"], inplace=True)
df_moyennes.rename(columns={"last24h_Vitesse": "Vitesse Moyenne", "last24h_VMG": "VMG Moyenne", "last24h_Distance":"Distance Moyenne", "rank": "Classement moyen"}, inplace=True)
df_moyennes
Out[25]:
Classement moyen Vitesse Moyenne VMG Moyenne Distance Moyenne DTF DTL
Skipper
Alan Roura 16.466783 11.870455 10.641608 284.864510 12010.866608 1805.306294
Alex Thomson 7.461538 12.698718 10.966667 304.442949 20795.096154 336.194231
Alexia Barrier 24.819403 9.940000 9.077015 238.259403 12213.304627 3417.251493
Ari Huusela 25.955777 9.788873 8.673752 234.952211 11909.323966 3480.116262
Armel Tripon 16.076772 13.003150 11.930118 312.056496 12658.633268 1227.416929
Arnaud Boissieres 17.452297 11.969788 10.724558 287.263604 12175.901060 1990.906890
Benjamin Dutreux 7.572301 13.424644 12.376986 322.044196 12213.708758 397.569857
Boris Herrmann 6.921649 13.664124 12.542268 327.923093 12291.379588 330.507629
Charlie Dalin 2.286902 13.828898 12.604990 331.893971 12072.658004 63.314137
Clarisse Cremer 13.508604 12.624665 11.652199 302.536711 12036.986042 924.411855
Clément Giraud 24.295492 11.000835 10.073957 264.040067 12751.698998 2991.381970
Damien Seguin 6.382716 13.376337 12.493827 320.936420 12267.142387 330.776337
Didac Costa 19.964041 11.585445 10.358904 277.675171 12245.223116 2245.323459
Fabrice Amedeo 26.965000 9.860500 8.594000 236.703500 21319.035500 2020.559000
Giancarlo Pedote 10.423868 13.558642 12.516461 325.390329 12385.792593 449.426955
Isabelle Joschke 11.510638 13.260638 12.247340 318.225266 15182.419415 531.471809
Jean Le Cam 5.629098 13.364754 12.408402 320.729303 12223.731557 335.812705
Jérémie Beyou 21.430427 12.884972 11.228015 309.184045 13378.487570 2581.681447
Kevin Escoffier 5.846715 13.644526 12.180292 327.411679 21178.533577 230.286131
Kojiro Shiraishi 22.084063 12.014536 10.595972 288.328196 12782.084238 2558.718039
Louis Burton 6.809524 13.774741 12.580952 330.648447 12338.296273 328.187992
Manuel Cousin 21.302251 10.951286 9.707395 262.840675 11895.654019 2485.311897
Maxime Sorel 10.659274 13.094960 12.246371 313.839919 12282.564718 583.285887
Miranda Merron 23.522167 10.888013 9.932512 261.288670 12528.256650 2924.392775
Nicolas Troussel 7.260000 11.238000 9.376000 269.396000 23428.510000 81.250000
Pip Hare 19.221254 11.649826 10.537805 279.559582 12273.868118 2103.756794
Romain Attanasio 13.415896 12.082440 11.196673 289.994085 11836.016266 1078.734381
Samantha Davies 10.337423 12.824540 10.688344 306.887117 20697.139877 422.485890
Stéphane Le Diraison 17.818499 11.932810 10.596684 286.359860 12248.694066 2060.894241
Sébastien Destremau 27.872902 8.872182 7.811031 212.909353 17639.015827 4004.966187
Sébastien Simon 10.398734 13.120886 11.296835 314.924684 20763.519620 356.931013
Thomas Ruyant 3.524793 13.796281 12.539256 330.988017 12153.609091 168.134091
Yannick Bestaven 4.374741 13.644306 12.527329 327.464803 12165.802277 155.693168
In [26]:
plt.figure(figsize=(15, 5))
linreg = sns.regplot(x = "VMG Moyenne", y = "Classement moyen", data = df_moyennes)
plt.title("Régression linéaire entre le classement et la vitesse utile des voiliers", fontweight='bold')
plt.xlim([df_moyennes["VMG Moyenne"].min()-0.05, df_moyennes["VMG Moyenne"].max()+0.05])
plt.show()

Les paramètres de cette droite de régression sont les suivants :

In [27]:
slope, intercept, r_value, p_value, std_err =  stats.linregress(x=linreg.get_lines()[0].get_xdata(),y=linreg.get_lines()[0].get_ydata())
reg = pd.DataFrame(columns=["Pente", "Ordonnée à l'origine", "r_value", "p_value", "Ecart-type"])
reg.loc[len(reg)] = [slope, intercept, r_value, p_value, std_err]
reg
Out[27]:
Pente Ordonnée à l'origine r_value p_value Ecart-type
0 -4.713204 66.34955 -1.0 0.0 1.003317e-08
In [28]:
coeff = np.corrcoef(df_moyennes["VMG Moyenne"], df_moyennes["Classement moyen"])[0][1]
print(f"Le coefficient de corrélation entre le classement et la VMG est de {coeff:.2f}")
Le coefficient de corrélation entre le classement et la VMG est de -0.83

Il existe donc bien une forte corrélation entre classement et vitesse utile. Les skippers avec le meilleur classement sont ceux qui ont la plus grande vitesse utile, en moyenne.

3.5.2 Impact de la présence d'un foil sur le classement et la vitesse des voiliers¶

Pour mesurer l'impact des foils, on sépare le jeu de données en 2 groupes : skippers avec foils, et skippers sans foil. Pour chacun des groupes, on va traver la distribution des classements moyens et des VMG moyennes pour conclure.

In [29]:
moyennes_merge_df = pd.merge(left=df_moyennes, right=df_boats, left_on="Skipper", right_on="skipper_name", how="left")
moyennes_merge_df = moyennes_merge_df.replace("2 asymétriques", "2")
moyennes_merge_df
Out[29]:
Classement moyen Vitesse Moyenne VMG Moyenne Distance Moyenne DTF DTL skipper_name skipper_id Nom du bateau Numéro de voile ... Date de lancement Longueur Largeur Tirant d'eau Déplacement (poids) Nombre de dérives Hauteur mât Voile quille Surface de voiles au près Surface de voiles au portant
0 16.466783 11.870455 10.641608 284.864510 12010.866608 1805.306294 Alan Roura 99 LA FABRIQUE SUI07 ... 01 Août 2007 18.28 5.90 4.5 foils 27.0 carbone 290.0 580.0
1 7.461538 12.698718 10.966667 304.442949 20795.096154 336.194231 Alex Thomson 122 HUGO BOSS GBR 99 ... 15 Août 2019 18.28 5.40 4.5 7.7 foils 29.0 acier forgé 330.0 630.0
2 24.819403 9.940000 9.077015 238.259403 12213.304627 3417.251493 Alexia Barrier 100 TSE - 4MYPLANET FRA72 ... 01 Mars 1998 18.28 5.54 4.5 9 2 29.0 acier 260.0 580.0
3 25.955777 9.788873 8.673752 234.952211 11909.323966 3480.116262 Ari Huusela 124 STARK FIN222 ... 06 Août 2007 18.28 5.80 4.5 8.5 2 28.0 acier 270.0 580.0
4 16.076772 13.003150 11.930118 312.056496 12658.633268 1227.416929 Armel Tripon 101 L'OCCITANE EN PROVENCE 2 ... 31 Janvier 2020 18.28 5.50 4.5 7.8 foils 28.0 acier forgé 270.0 535.0
5 17.452297 11.969788 10.724558 287.263604 12175.901060 1990.906890 Arnaud Boissieres 88 LA MIE CÂLINE - ARTISANS ARTIPÔLE FRA 14 ... 03 Août 2007 18.28 5.65 4.5 7.9 foils 29.0 basculante avec vérin 300.0 610.0
6 7.572301 13.424644 12.376986 322.044196 12213.708758 397.569857 Benjamin Dutreux 112 OMIA - WATER FAMILY FRA09 ... 03 Juillet 2007 18.28 5.85 4.5 8 2 29.0 acier forgé 300.0 700.0
7 6.921649 13.664124 12.542268 327.923093 12291.379588 330.507629 Boris Herrmann 109 SEAEXPLORER - YACHT CLUB DE MONACO 16 ... 07 Août 2015 18.28 5.70 4.5 7.6 foils 29.0 acier 290.0 490.0
8 2.286902 13.828898 12.604990 331.893971 12072.658004 63.314137 Charlie Dalin 105 APIVIA FRA 79 ... 05 Août 2019 18.28 5.85 4.5 8 foils 29.0 acier 350.0 560.0
9 13.508604 12.624665 11.652199 302.536711 12036.986042 924.411855 Clarisse Cremer 95 BANQUE POPULAIRE X FRA30 ... 01 Mars 2011 18.28 5.70 4.5 7.7 2 29.0 acier forgé 340.0 570.0
10 24.295492 11.000835 10.073957 264.040067 12751.698998 2991.381970 Clément Giraud 114 Compagnie du Lit / Jiliti FRA83 ... 26 Juillet 2006 18.28 5.75 4.5 8.5 2 29.0 acier forgé 300.0 620.0
11 6.382716 13.376337 12.493827 320.936420 12267.142387 330.776337 Damien Seguin 111 GROUPE APICIL FRA1000 ... 10 Août 2008 18.28 5.85 4.5 8.5 2 29.0 acier forgé 350.0 610.0
12 19.964041 11.585445 10.358904 277.675171 12245.223116 2245.323459 Didac Costa 123 ONE PLANET ONE OCEAN ESP 33 ... 02 Février 2000 18.28 5.30 4.5 8.9 2 26.0 acier 240.0 470.0
13 26.965000 9.860500 8.594000 236.703500 21319.035500 2020.559000 Fabrice Amedeo 93 NEWREST - ART & FENÊTRES FRA 56 ... 01 Août 2015 18.28 5.85 4.5 7 foils 29.0 monotype 320.0 570.0
14 10.423868 13.558642 12.516461 325.390329 12385.792593 449.426955 Giancarlo Pedote 96 PRYSMIAN GROUP ITA 34 ... 02 Avril 2015 18.28 5.80 4.5 8 foils 29.0 acier forgé 300.0 600.0
15 11.510638 13.260638 12.247340 318.225266 15182.419415 531.471809 Isabelle Joschke 102 MACSF FRA 27 ... 04 Juillet 2007 18.28 5.60 4.5 7.7 foils 27.0 acier 300.0 650.0
16 5.629098 13.364754 12.408402 320.729303 12223.731557 335.812705 Jean Le Cam 116 Yes We Cam! 001 ... 03 Janvier 2007 18.28 5.90 4.5 8 2 28.0 acier forgé 300.0 620.0
17 21.430427 12.884972 11.228015 309.184045 13378.487570 2581.681447 Jérémie Beyou 103 CHARAL 08 ... 18 Août 2018 18.28 5.85 4.5 8 foils 29.0 acier 320.0 600.0
18 5.846715 13.644526 12.180292 327.411679 21178.533577 230.286131 Kevin Escoffier 89 PRB FRA 85 ... 08 Mars 2010 18.28 5.50 4.5 foils 27.4 Acier mécano soudé 300.0 600.0
19 22.084063 12.014536 10.595972 288.328196 12782.084238 2558.718039 Kojiro Shiraishi 119 DMG MORI Global One JPN 11 ... 05 Septembre 2019 18.28 5.85 4.5 8 foils 29.0 acier forgé 320.0 580.0
20 6.809524 13.774741 12.580952 330.648447 12338.296273 328.187992 Louis Burton 106 BUREAU VALLEE 2 18 ... 09 Juin 2015 18.28 5.80 4.5 7.6 foils 28.0 acier 300.0 600.0
21 21.302251 10.951286 9.707395 262.840675 11895.654019 2485.311897 Manuel Cousin 92 GROUPE SÉTIN FRA 71 ... 02 Février 2007 18.28 5.80 4.5 9 2 28.5 basculante sur vérin hydraulique 270.0 560.0
22 10.659274 13.094960 12.246371 313.839919 12282.564718 583.285887 Maxime Sorel 107 V and B-MAYENNE FRA53 ... 07 Septembre 2007 18.28 5.50 4.5 7.7 2 29.0 acier 365.0 700.0
23 23.522167 10.888013 9.932512 261.288670 12528.256650 2924.392775 Miranda Merron 104 CAMPAGNE DE FRANCE FRA50 ... 04 Mai 2006 18.28 5.50 4.5 8.5 2 28.0 carbone 330.0 600.0
24 7.260000 11.238000 9.376000 269.396000 23428.510000 81.250000 Nicolas Troussel 108 CORUM L'EPARGNE FRA 6 ... 15 Mai 2020 18.28 5.70 4.5 7.9 foils 27.3 NaN 270.0 535.0
25 19.221254 11.649826 10.537805 279.559582 12273.868118 2103.756794 Pip Hare 120 MEDALLIA GBR77 ... 03 Juillet 1999 18.28 5.70 4.5 9 2 29.0 carbone 300.0 580.0
26 13.415896 12.082440 11.196673 289.994085 11836.016266 1078.734381 Romain Attanasio 110 PURE - Best Western® FRA 49 ... 08 Mars 2007 18.28 5.80 4.5 9 2 28.0 acier forgé 280.0 560.0
27 10.337423 12.824540 10.688344 306.887117 20697.139877 422.485890 Samantha Davies 97 INITIATIVES-COEUR FRA109 ... 20 Septembre 2010 18.28 5.70 4.5 7.8 foils 27.0 acier forgé 300.0 600.0
28 17.818499 11.932810 10.596684 286.359860 12248.694066 2060.894241 Stéphane Le Diraison 94 TIME FOR OCEANS FRA 92 ... 01 Juin 2007 18.28 5.84 4.5 8.5 foils 28.0 monotype 300.0 550.0
29 27.872902 8.872182 7.811031 212.909353 17639.015827 4004.966187 Sébastien Destremau 117 MERCI 69 ... 15 Janvier 2005 18.28 5.60 4.5 8.5 2 29.0 acier 250.0 650.0
30 10.398734 13.120886 11.296835 314.924684 20763.519620 356.931013 Sébastien Simon 90 ARKEA PAPREC 4 ... 19 Juillet 2019 18.28 5.70 4.5 8 foils 29.0 Inox usiné 260.0 600.0
31 3.524793 13.796281 12.539256 330.988017 12153.609091 168.134091 Thomas Ruyant 113 LinkedOut NaN ... 03 Septembre 2019 18.28 5.85 4.5 8 foils 29.0 acier forgé 350.0 560.0
32 4.374741 13.644306 12.527329 327.464803 12165.802277 155.693168 Yannick Bestaven 91 Maître CoQ IV 17 ... 12 Mars 2015 18.28 5.80 4.5 8 foils 29.0 acier mécano soudé 310.0 550.0

33 rows × 23 columns

In [30]:
foils = moyennes_merge_df[moyennes_merge_df["Nombre de dérives"] == "foils"]
no_foil = moyennes_merge_df[moyennes_merge_df["Nombre de dérives"] != "foils"]
In [31]:
plt.figure(figsize=(8, 4))
sns.kdeplot(foils["Classement moyen"], bw=0.3, label="Avec foils", fill=True)
plt.axvline(x = foils['Classement moyen'].mean(), color = 'C0', label ="Classement moyen avec foils", linestyle="dotted")
sns.kdeplot(no_foil["Classement moyen"], bw=0.3, label="Sans foil", fill=True)
plt.axvline(x = no_foil['Classement moyen'].mean(), color = 'C1', label ="Classement moyen sans foil", linestyle="dotted")
plt.title("Densité de répartition du classement des skippers avec/sans foil sur leur voilier")
plt.legend(loc="best")
plt.show()

Valeurs numériques

In [32]:
print(f"Classement moyen avec foils         : {foils['Classement moyen'].mean():.2f}")
print(f"Classement moyen sans foil          : {no_foil['Classement moyen'].mean():.2f}")
print(f"Ecart-type du classement avec foils : {foils['Classement moyen'].std():.2f}")
print(f"Ecart-type du classement sans foil  : {no_foil['Classement moyen'].std():.2f}")
Classement moyen avec foils         : 11.87
Classement moyen sans foil          : 17.44
Ecart-type du classement avec foils : 6.99
Ecart-type du classement sans foil  : 7.75

On remarque donc, qu'à dispertion similaire, le classement moyen des skippers avec foils est bien inférieur à celui des skippers sans foil !

In [33]:
plt.figure(figsize=(8, 4))
sns.kdeplot(foils["VMG Moyenne"], bw=0.4, label="Avec foils", fill=True)
plt.axvline(x = foils["VMG Moyenne"].mean(), color = 'C0', label ="VMG moyenne avec foils", linestyle="dotted")
sns.kdeplot(no_foil["VMG Moyenne"], bw=0.4, label="Sans foil", fill=True)
plt.axvline(x = no_foil["VMG Moyenne"].mean(), color = 'C1', label ="VMG moyenne sans foil", linestyle="dotted")
plt.title("Densité de répartition de VMG des skippers avec/sans foil sur leur voilier")
plt.legend(loc="upper left")
plt.show()
In [34]:
print(f"VMG moyenne avec foils          : {foils['VMG Moyenne'].mean():.2f}")
print(f"VMG moyenne sans foil           : {no_foil['VMG Moyenne'].mean():.2f}")
print(f"Ecart-type de la VMG avec foils : {foils['VMG Moyenne'].std():.2f}")
print(f"Ecart-type de la VMG sans foil  : {no_foil['VMG Moyenne'].std():.2f}")
VMG moyenne avec foils          : 11.39
VMG moyenne sans foil           : 10.61
Ecart-type de la VMG avec foils : 1.17
Ecart-type de la VMG sans foil  : 1.51

De même que pour les classements, on remarque donc, qu'à dispertion similaire, la VMG des skippers avec foils est bien supérieure à celle des skippers sans foil !

On peut retrouver ces résultats sur la courbe de comparaison classement / VMG sur le graphe ci-dessous

In [35]:
sns.jointplot(data=moyennes_merge_df, x="Classement moyen", y="VMG Moyenne", hue="Nombre de dérives", height=6, kind="scatter")
plt.show()

La présence de foils sur le voilier semble donc bien être un facteur favorisant un bon classement.

3.6. Analyses de séries temporelles¶

3.6.1 Méthode générale d'analyse¶

Les données sont fournies à intervalles temporels réguliers, il est donc intéressant de se poser la question de leur analyse temporelle. Intéressons-nous aux données d'un participant en particulier pour essayer de trouver des caractéristiques intéressantes, Yannick Bestave par exemple.

Visualisation de l'évolution du rang et de la VMG au long de la course

In [36]:
yan = df_running[df_running["Skipper"] == "Yannick Bestaven"].reset_index(drop=True)
plt.rcParams['figure.figsize'] = [18, 4]
fig, axs = plt.subplots(1, 2)
axs[0].plot(yan["datetime"],yan["rank"])
axs[0].set_xlabel("Date")
axs[0].set_ylabel("Rang")
axs[0].axhline(y=yan["rank"].mean(), label="Classement moyen", color="C1")
axs[0].legend()
axs[1].plot(yan["datetime"],yan["last24h_VMG"])
axs[1].set_xlabel("Date")
axs[1].set_ylabel("VMG")
axs[1].axhline(y=yan["last24h_VMG"].mean(), label="VMG moyenne", color="C1")
axs[1].legend()
plt.show()

Il ne semble pas y avoir de saisonnalité dans les données de classement et de VMG (caractéristiques qui nous intéressent le plus).
Y a-t-il un lien entre le rang à un instant $t$ et celui à un instant $t+1$ ? entre la VMG à un instant $t$ et celle à un instant $t+1$? Traçons quelques lag plots pour estimer cela.
De gauche à droite sont représentés le rang à un instant $t+1$, $t+2$, $t+3$, $t+4$ et $t+5$ à partir du rang à un instant $t$.

In [37]:
plt.rcParams['figure.figsize'] = [20, 4]
fig, axs = plt.subplots(1, 5)
pd.plotting.lag_plot(yan["rank"], lag=1, ax=axs[0])
pd.plotting.lag_plot(yan["rank"], lag=2, ax=axs[1])
pd.plotting.lag_plot(yan["rank"], lag=3, ax=axs[2])
pd.plotting.lag_plot(yan["rank"], lag=4, ax=axs[3])
pd.plotting.lag_plot(yan["rank"], lag=5, ax=axs[4])
plt.show()

On observe bien une corrélation entre le rang à un instant $t$ et $t+1$, corrélation qui diminue lorsqu'on augmente le lag. On observe également plus de dispersion aux alentours des rangs élevés, cette corrélation est donc probablement meilleure pour les meilleurs skippers.

In [38]:
plt.rcParams['figure.figsize'] = [15, 4]
fig, axs = plt.subplots(1, 4)
pd.plotting.lag_plot(yan["last24h_VMG"], lag=1, ax=axs[0])
axs[0].set_title("Lag = 1, VMG sur 24h")
pd.plotting.lag_plot(yan["last24h_VMG"], lag=2, ax=axs[1])
axs[1].set_title("Lag = 2, VMG sur 24h")
pd.plotting.lag_plot(yan["lastranking_VMG"], lag=1, ax=axs[2])
axs[2].set_title("Lag = 1, VMG last ranking")
pd.plotting.lag_plot(yan["lastranking_VMG"], lag=2, ax=axs[3])
axs[3].set_title("Lag = 2, VMG last ranking")
plt.show()

Au niveau de la VMG, on observe une corrélation encore plus forte qu'avec le rang. Cette corrélatione est bien meilleure lorsqu'on utilise la VMG calculée sur 24h plutôt qu'entre chaque classement.

La fonction plot_acf de la libray statsmodels permet de tracer la fonction d'autocorrélation (ACF) et la fonction d'autocorrélation partielle (PACF) des variables souhaitées (importance des variables aux instants t-1, t-2, ... t-n sur la variable à l'instant t). Sur les graphes ci-dessous, les points dans les zones bleues sont sous le seuil significatif, et sont donc considérés comme n'ayant pas de poids sur l'observation à l'instant t.

In [39]:
plt.rcParams['figure.figsize'] = [15, 4]
fig, axs = plt.subplots(1, 2)
plot_acf(yan["rank"], ax=axs[0]);
axs[0].set_title("ACF du classement")
plot_acf(yan["last24h_VMG"], ax=axs[1]);
axs[1].set_title("ACF de la VMG")
plt.show()

Le classement ne semble donc pas idéal pour appliquer un modèle ARIMA, mais cela semble possible avec la VMG.
Vérifions cela de manière plus formelle avec un test augmenté de Dickey-Fuller.

In [40]:
plt.rcParams['figure.figsize'] = [15, 4]
fig, axs = plt.subplots(1, 2)
plot_pacf(yan["rank"], ax=axs[0]);
axs[0].set_title("PACF du classement")
plot_pacf(yan["last24h_VMG"], ax=axs[1]);
axs[1].set_title("PACF de la VMG")
plt.show()

La PACF révèle que pour la VMG, il existe une forte corrélation entre la VMG à un instant t, t-1 et également t-2.

In [41]:
def check_stationarity(series):
    result = adfuller(series.values)
    print('ADF Statistic: %f' % result[0])
    print('p-value: %f' % result[1])
    print('Critical Values:')
    for key, value in result[4].items():
        print('\t%s: %.3f' % (key, value))

    if (result[1] <= 0.05) & (result[4]['5%'] > result[0]):
        print("\u001b[32mStationary\u001b[0m")
    else:
        print("\x1b[31mNon-stationary\x1b[0m")
In [42]:
check_stationarity(yan["rank"])
ADF Statistic: -1.385071
p-value: 0.589412
Critical Values:
	1%: -3.444
	5%: -2.868
	10%: -2.570
Non-stationary

Appliquer un modèle ARIMA au classement n'est donc pas pertinent, pour ce skipper tout du moins.

In [43]:
check_stationarity(yan["last24h_VMG"])
ADF Statistic: -3.662662
p-value: 0.004670
Critical Values:
	1%: -3.444
	5%: -2.868
	10%: -2.570
Stationary

3.6.2. Prédiction de VMG¶

Au vu des résultats, nous allons essayer d'appliquer un modèle ARIMA pour prédire la VMG du skipper Yannick Bestaven

Les paramètres p et q à choisir dépendent des résultats de l'ACF et PACF :

  • p correspond au nombre de spikes significatifs observés sur la PACF : on peut essayer 2 ou 7 ici (7 étant le dernier spike en dehors de la zone bleue sur la PACF de la VMG)
  • d correspond au nombre de différenciation nécessaire pour obtenir des données stationnaires : 0 dans notre cas
  • q correspond au nombre de spikes significatifs observés sur l'ACF : on peut essayer 13 ici
In [44]:
print(f"Nombre de classements où Yannick Bestaven est encore en course : {len(yan['datetime'].unique().tolist())}")
Nombre de classements où Yannick Bestaven est encore en course : 483
In [45]:
arima1 = ARIMA(yan["last24h_VMG"][:300], order=(2,0,13))
arima2 = ARIMA(yan["last24h_VMG"][:300], order=(7,0,13))
model1 = arima1.fit()
model2 = arima2.fit()
In [46]:
model1.summary()
Out[46]:
SARIMAX Results
Dep. Variable: last24h_VMG No. Observations: 300
Model: ARIMA(2, 0, 13) Log Likelihood -314.466
Date: Sun, 04 Dec 2022 AIC 662.932
Time: 23:34:07 BIC 725.896
Sample: 0 HQIC 688.130
- 300
Covariance Type: opg
coef std err z P>|z| [0.025 0.975]
const 12.4261 1.691 7.347 0.000 9.111 15.741
ar.L1 1.2978 1.731 0.750 0.453 -2.095 4.690
ar.L2 -0.3294 1.628 -0.202 0.840 -3.521 2.862
ma.L1 0.2551 1.735 0.147 0.883 -3.145 3.655
ma.L2 0.1189 1.058 0.112 0.911 -1.955 2.193
ma.L3 0.2833 0.608 0.466 0.641 -0.908 1.475
ma.L4 0.1523 0.736 0.207 0.836 -1.290 1.595
ma.L5 0.0727 0.562 0.129 0.897 -1.028 1.174
ma.L6 -0.5645 0.362 -1.561 0.119 -1.273 0.144
ma.L7 -0.0041 0.825 -0.005 0.996 -1.622 1.614
ma.L8 0.0249 0.344 0.072 0.942 -0.649 0.699
ma.L9 -0.0562 0.102 -0.549 0.583 -0.257 0.144
ma.L10 0.0680 0.127 0.535 0.593 -0.181 0.317
ma.L11 0.0612 0.159 0.386 0.700 -0.250 0.372
ma.L12 -0.1313 0.193 -0.680 0.497 -0.510 0.247
ma.L13 -0.0323 0.168 -0.192 0.848 -0.361 0.297
sigma2 0.4589 0.029 15.715 0.000 0.402 0.516
Ljung-Box (L1) (Q): 0.21 Jarque-Bera (JB): 342.81
Prob(Q): 0.64 Prob(JB): 0.00
Heteroskedasticity (H): 0.77 Skew: -1.14
Prob(H) (two-sided): 0.19 Kurtosis: 7.72


Warnings:
[1] Covariance matrix calculated using the outer product of gradients (complex-step).
In [47]:
model2.summary()
Out[47]:
SARIMAX Results
Dep. Variable: last24h_VMG No. Observations: 300
Model: ARIMA(7, 0, 13) Log Likelihood -311.786
Date: Sun, 04 Dec 2022 AIC 667.572
Time: 23:34:08 BIC 749.055
Sample: 0 HQIC 700.182
- 300
Covariance Type: opg
coef std err z P>|z| [0.025 0.975]
const 12.4453 1.672 7.444 0.000 9.169 15.722
ar.L1 1.5751 0.499 3.156 0.002 0.597 2.553
ar.L2 -0.7418 0.965 -0.769 0.442 -2.633 1.149
ar.L3 0.1411 0.828 0.171 0.865 -1.481 1.763
ar.L4 -0.3255 0.747 -0.436 0.663 -1.789 1.138
ar.L5 0.5716 0.719 0.795 0.427 -0.837 1.981
ar.L6 -0.7531 0.625 -1.206 0.228 -1.977 0.471
ar.L7 0.4759 0.284 1.678 0.093 -0.080 1.032
ma.L1 -0.0402 0.507 -0.079 0.937 -1.034 0.954
ma.L2 0.0939 0.329 0.285 0.775 -0.551 0.739
ma.L3 0.2651 0.295 0.899 0.369 -0.313 0.843
ma.L4 0.4145 0.309 1.340 0.180 -0.192 1.021
ma.L5 -0.0087 0.271 -0.032 0.974 -0.539 0.522
ma.L6 -0.1145 0.235 -0.487 0.627 -0.576 0.347
ma.L7 0.5527 0.254 2.174 0.030 0.054 1.051
ma.L8 0.1834 0.294 0.624 0.532 -0.392 0.759
ma.L9 0.1278 0.259 0.493 0.622 -0.380 0.636
ma.L10 0.0243 0.244 0.099 0.921 -0.455 0.503
ma.L11 0.2427 0.254 0.957 0.339 -0.254 0.740
ma.L12 -0.3794 0.128 -2.964 0.003 -0.630 -0.129
ma.L13 -0.1209 0.167 -0.723 0.470 -0.449 0.207
sigma2 0.4493 0.030 14.889 0.000 0.390 0.508
Ljung-Box (L1) (Q): 0.23 Jarque-Bera (JB): 324.17
Prob(Q): 0.63 Prob(JB): 0.00
Heteroskedasticity (H): 0.78 Skew: -1.19
Prob(H) (two-sided): 0.22 Kurtosis: 7.50


Warnings:
[1] Covariance matrix calculated using the outer product of gradients (complex-step).

Les coefficients AIC, BIC et HQIC sont plus faibles sur le modèle 1 (avec p=2). On retient donc ce modèle pour nos prévisions.

Le graphe ci-dessous affiche les valeurs restituées par le modèle sur les données d'apprentissage en orange, et celles prédites pour les 183 classements restants en vert.
On remarque que le modèle a bien appris les valeurs : la courbe réelle est toujours incluse dans l'intervalle de confiance à 95%. En revanche, les valeurs prédites divergent très vite des valeurs réelles...

In [48]:
forecast = model1.forecast(183)
plt.rcParams['figure.figsize'] = [20, 8]
fig, ax = plt.subplots()
ax.plot(yan["last24h_VMG"], label="VMG réelle")
plot_predict(model1, ax=ax)
ax.plot(np.arange(300,483), forecast, label="VMG prédite par le modèle")
plt.legend()
plt.show()

Pour quantifier cela un peu mieux, on peut tracer le carré de l'erreur de prédiction. On observe que le modèle est plutôt bon sur les 15 premières prédictions, valeur à partir de laquelle il diverge.

In [49]:
plt.rcParams['figure.figsize'] = [15, 6]
error = yan["last24h_VMG"][300:].to_numpy() - forecast
plt.plot(error**2, label="Erreur de prédiction au carré")
plt.axvline(x = 315, color ='C1', linestyle="dotted", label="15 relevés de classement après la dernière valeur apprise")
plt.legend()
plt.show()

On peut essayer d'entraîner le modèle sur plus de points pour voir s'il arrive à réaliser des prédictions à plus longue portée : entraînement sur 400 points et 450 points (puis prédiction sur 83 et 33 points).

In [50]:
arima400 = ARIMA(yan["last24h_VMG"][:400], order=(2,0,13))
model400 = arima400.fit()
arima450 = ARIMA(yan["last24h_VMG"][:450], order=(2,0,13))
model450 = arima450.fit()
forecast400 = model400.forecast(83)
forecast450 = model450.forecast(33)
In [51]:
plt.rcParams['figure.figsize'] = [25, 6]
fig, ax = plt.subplots(1, 2)
fig.subplots_adjust(wspace=0, hspace=0)
ax[0].plot(yan["last24h_VMG"], label="VMG réelle")
plot_predict(model400, ax=ax[0])
ax[0].plot(np.arange(400,483), forecast400, label="VMG prédite par le modèle")
ax[0].legend()
ax[1].plot(yan["last24h_VMG"], label="VMG réelle")
plot_predict(model450, ax=ax[1])
ax[1].plot(np.arange(450,483), forecast450, label="VMG prédite par le modèle")
ax[1].legend()
plt.show()
In [52]:
plt.rcParams['figure.figsize'] = [15, 4]
fig, ax = plt.subplots(1, 2)
error400 = yan["last24h_VMG"][400:].to_numpy() - forecast400
error450 = yan["last24h_VMG"][450:].to_numpy() - forecast450
ax[0].plot(error400**2, label="Erreur de prédiction au carré")
ax[1].plot(error450**2, label="Erreur de prédiction au carré")
ax[0].axvline(x = 415, color ='C1', linestyle="dotted", label="15 relevés de classement après la dernière valeur apprise")
ax[1].axvline(x = 465, color ='C1', linestyle="dotted", label="15 relevés de classement après la dernière valeur apprise")
ax[0].legend()
ax[1].legend()
plt.show()

On ne remarque pas d'amélioration notable des prédictions (même une déterioration).
Ceci est prévisible, car il n'y a pas de saisonnalité dans les données pouvant aider les prédictions. De ce point de vue, une prédiction de vitesse 15 relevés de classement en avance est déjà un résultat intéressant.

3.6.3. Prédiction de classement¶

On peut également regarder les valeurs de rang d'autres skippers pour voir si certains sont stationnaires. Prenons l'exemple de Maxime Sorel

In [53]:
sor = df_running[df_running["Skipper"] == "Maxime Sorel"].reset_index(drop=True)
plt.rcParams['figure.figsize'] = [18, 4]
fig, axs = plt.subplots(1, 2)
axs[0].plot(sor["datetime"],sor["rank"])
axs[0].set_xlabel("Date")
axs[0].set_ylabel("Rang")
axs[0].axhline(y=sor["rank"].mean(), label="Classement moyen", color="C1")
axs[0].legend()
axs[1].plot(sor["datetime"],sor["last24h_VMG"])
axs[1].set_xlabel("Date")
axs[1].set_ylabel("VMG")
axs[1].axhline(y=sor["last24h_VMG"].mean(), label="VMG moyenne", color="C1")
axs[1].legend()
plt.show()
In [54]:
plt.rcParams['figure.figsize'] = [15, 4]
fig, axs = plt.subplots(1, 2)
plot_acf(sor["rank"], ax=axs[0]);
axs[0].set_title("ACF du classement")
plot_acf(sor["last24h_VMG"], ax=axs[1]);
axs[1].set_title("ACF de la VMG")
plt.show()
In [55]:
plt.rcParams['figure.figsize'] = [15, 4]
fig, axs = plt.subplots(1, 2)
plot_pacf(sor["rank"], ax=axs[0]);
axs[0].set_title("PACF du classement")
plot_pacf(sor["last24h_VMG"], ax=axs[1]);
axs[1].set_title("PACF de la VMG")
plt.show()
In [56]:
check_stationarity(sor["rank"])
ADF Statistic: -4.673052
p-value: 0.000095
Critical Values:
	1%: -3.444
	5%: -2.868
	10%: -2.570
Stationary

Cette fois, le rang est stationnaire. On peut essayer de prédire les futurs classement de Maxime Sorel par la même méthode que précédemment.

D'après les graphes ci-dessus, on choisit les valeurs suivantes pour les paramètres d'arima:

  • p = 4 (sur la PACF)
  • d = 0
  • q = 19 (sur l'ACF)
In [57]:
print(f"Nombre de classements où Maxime Sorel est encore en course : {len(sor['datetime'].unique().tolist())}")
Nombre de classements où Maxime Sorel est encore en course : 496
In [58]:
arima = ARIMA(sor["rank"][:300], order=(4,0,19))
model = arima.fit()
forecast = model.forecast(196)
In [59]:
plt.rcParams['figure.figsize'] = [10, 4]
fig, ax = plt.subplots()
ax.plot(sor["rank"], label="Classement réel")
plot_predict(model, ax=ax)
ax.plot(np.arange(300,496), forecast, label="Classement prédit par le modèle")
plt.legend()
plt.show()
In [60]:
print(f"Dernière valeur prédite par le modèle    : {np.round(forecast.iloc[-1])}")
print(f"Dernière classement réel de Maxime Sorel : {sor['rank'].iloc[-1]}")
Dernière valeur prédite par le modèle    : 11.0
Dernière classement réel de Maxime Sorel : 10.0

Le modèle ne prédit par la valeur exacte, mais n'est pas très loin du vrai résultat.
Par curiosité, tentons également de prédire la VMG de ce skipper avec les paramètre ARIMA suivants :

  • p = 4 (PACF)
  • d = 0
  • q = 8 (ACF)
In [61]:
arima = ARIMA(sor["last24h_VMG"][:300], order=(4,0,8))
model = arima.fit()
forecast = model.forecast(196)
In [62]:
plt.rcParams['figure.figsize'] = [10, 4]
fig, ax = plt.subplots()
ax.plot(sor["last24h_VMG"], label="VMG réelle")
plot_predict(model, ax=ax)
ax.plot(np.arange(300,496), forecast, label="VMG prédite par le modèle")
plt.legend()
plt.show()